<?php

abstract class Kernel_Form_Order_Abstract extends Kernel_Component 
{
    
    /**
    * @desc Table where values are stored. 
    * 
    * @param string
    */
    public $table; 
    
    /**
    * @desc Table where values are stored. 
    * 
    * @param string
    */
    public $parent; 
    
    /**
     * @desc Name of the unique identifier field. 
     *
     * @param string
     */
    public $id_field = 'id';
    
    /**
     * @desc Name of the position field. 
     *
     * @param string
     */
    public $pos_field = 'position';
    
    /**
     * @desc Name of the unique identifier field. 
     *
     * @param string
     */
    public $parent_field = 'parent_id';
    
    /**
     * @desc SQL WHERE statement. 
     *
     * @param string
     */
    public $condition = array();
    
    
    
    public function __construct($params = array()) 
    {
        if (true !== array_key_exists('table', $params)) {
            trigger_error('Table must be specified in class definition', E_USER_ERROR);
        }
        
        parent::__construct($params);
    }
    
    /**
    * @desc Return simple order object or the one to work with parents.
    * 
    * @param string $type
    * @param array  $params
    * @return object 
    */
    static public function factory($type, $params) 
    {
        return Kernel::factory('Kernel_Form_Order', ucfirst($type), $params);
    }
    
    /**
    * @desc Add node to the specified position. 
    * 
    * @param integer $position 
    * @param array $addit_values = array() 
    */
    protected function _addNode($position, $addit_values = array()) 
    {
        $values = array_slice($this->_getValues(), $position-1);
        $this->_increment($values);
        
        try {
            $insert_id = database::insert($this->table, array($this->pos_field => $position) + $addit_values);
        // catch exception of duplicate param
        } catch (Kernel_Db_Exception $e) {
            if (MySQL::ER_DUP_ENTRY == $e->getCode()) {
                // get back changed positions 
                $this->_decrement($values);
                // and pass it higher by callstack to form link on duplicate entry
                throw $e;
            }
        }
        
        return $insert_id;
    }
    
    /**
    * @desc Move specified node on position specified.
    * 
    * @param integer $_id
    * @param integer $_position
    */
    protected function _moveNode($_id, $_position) 
    {
        $position = $_position - 1;
        $current = array_search($_id, $values = $this->_getValues());
        
        // verify initial conditions
        if ($position == $current || $_id < 1) { return; }
        
        
        // temporary zerofill editable element position to avoid unique key conflict
        database::update($this->table, 
            array($this->pos_field => 0), 
            array($this->id_field => $_id) + $this->condition);
        
        switch (true) 
        {
            case $current > $position :
                $values = array_slice($values, $position, $current - $position);
                $func = '_increment';
                break;
                
            case $current < $position :
                $values = array_slice($values, $current + 1, $position - $current);
                $func = '_decrement';
                break;
        }
        
        call_user_func(array($this, $func), $values);
        
        database::update($this->table, 
            array($this->pos_field => $_position), array($this->id_field => $_id) + $this->condition);
    }
    
    protected function _deleteNode($id) 
    {
        // get position of the editable element before it'll be deleted
        $position = $this->_getPosition($id);
        
        $affected_rows = database::delete($this->table, array($this->id_field => $id) + $this->condition);
        
        $this->_decrement(array_slice($this->_getValues(), $position - 1));
        
        return $affected_rows;
    }
    
    /**
    * @desc Return values selected by function _getQueryBody(). Also control 
    * order sampling. 
    * 
    * @return array
    */
    protected function _getValues() 
    {
        if (false === stripos($query = $this->_getQueryBody(), 'ASC')) {
            trigger_error('Order must be ascending', E_USER_ERROR);
        }
        return database::fetchCol($query);
    }
    
    /**
     * @desc Form SQl statement. 
     *
     * @return  string
     */
    abstract protected function _getQueryBody();
    
    /**
    * @desc Decrement given values positions. 
    * 
    * @param array $vales
    */
    protected function _decrement($values) 
    {
        for ($i=0; $i<count($values); $i++) {
            database::update($this->table, 
                array($this->pos_field => database::expression('`position` - 1')), 
                array($this->id_field => $values[$i]) + $this->condition);
        }
    }
    
    /**
    * @desc Increment given values positions. 
    * 
    * @param array $vales
    */
    protected function _increment($values) 
    {
        $i = count($values);
        while ($i-- > 0) {
            database::update($this->table, 
                array($this->pos_field => database::expression('`position` + 1')), 
                array($this->id_field => $values[$i]) + $this->condition);
        }
    }
    
    /**
    * @desc Return specified node position. 
    * 
    * @return integer
    */
    protected function _getPosition($id) 
    {
        $query = '
            SELECT 
                   `' . $this->pos_field . '` 
              FROM 
                   `' . $this->table . '` 
             WHERE 
                   `' . $this->id_field . '` = ' . (int)$id;
        return database::fetchOne($query);
    }
    
    
    
}